Skip to content

feat(ui): add scroll-to-bottom button and improve scroll navigation#1491

Merged
mergify[bot] merged 1 commit intomainfrom
jsell/feat/scroll-navigation
May 2, 2026
Merged

feat(ui): add scroll-to-bottom button and improve scroll navigation#1491
mergify[bot] merged 1 commit intomainfrom
jsell/feat/scroll-navigation

Conversation

@jsell-rh
Copy link
Copy Markdown
Contributor

@jsell-rh jsell-rh commented May 2, 2026

Summary

Extends #1483. Related to #1482.

  • Add scroll-to-bottom button alongside existing scroll-to-top in session message view
  • Both buttons animate in/out smoothly (no abrupt pop-in) with tooltips
  • Fix ghost tooltip appearing when buttons are hidden (force-close via controlled open prop)
  • Fix scroll-to-bottom button showing on short content that doesn't overflow
  • Add spec: specs/frontend/sessions/messages/scroll-navigation.spec.md

Test plan

  • Open a session with enough messages to scroll
  • Scroll down past threshold — scroll-to-top button fades in
  • Scroll up from bottom — scroll-to-bottom button fades in
  • Click scroll-to-top — smooth scroll, button fades out
  • Click scroll-to-bottom — smooth scroll, button fades out
  • Hover each button — tooltip appears on left side
  • Click button then move mouse out/in — no ghost tooltip
  • Open session with few messages — neither button appears
  • During streaming, verify auto-scroll stays instant (no jank)

🤖 Generated with Claude Code

…n session view

Add a scroll-to-bottom button alongside the existing scroll-to-top button
in the session message view. Both buttons now animate smoothly, have
tooltips, and correctly hide when content doesn't overflow. Fixes ghost
tooltip appearing when buttons are hidden.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jsell-rh jsell-rh added the ambient-code:self-reviewed Self-reviewed by Ambient agent label May 2, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented May 2, 2026

Deploy Preview for cheerful-kitten-f556a0 canceled.

Name Link
🔨 Latest commit 1a8659d
🔍 Latest deploy log https://app.netlify.com/projects/cheerful-kitten-f556a0/deploys/69f55a8b12e81500086458ab

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

📝 Walkthrough

Walkthrough

This PR adds bidirectional scroll navigation to the messages tab. The scroll detection logic is fixed to handle non-overflowing content, and the UI is updated to display both scroll-to-top and scroll-to-bottom buttons with tooltips and smooth scrolling behavior. Accompanying specification documents the requirements.

Changes

Bidirectional Scroll Navigation

Layer / File(s) Summary
Scroll Detection
components/frontend/src/components/session/MessagesTab.tsx
checkIfAtBottom now returns true immediately when container content does not overflow, preventing incorrect "not at bottom" state when scrolling is unnecessary.
UI Components & Imports
components/frontend/src/components/session/MessagesTab.tsx
Added ChevronDown icon import and Radix UI tooltip component imports. Replaced single conditional scroll-to-top button with tooltip-wrapped bidirectional scroll UI: animated scroll-to-top (controlled by showScrollToTop) and scroll-to-bottom button (controlled by !isAtBottom) with smooth scroll behavior and tooltips on both.
Specification
specs/frontend/sessions/messages/scroll-navigation.spec.md
Documents scroll button visibility thresholds, hide behavior for non-overflowing content, smooth animated show/hide transitions with disabled pointer/tooltip interactions while hidden, click-to-scroll behavior, instant auto-scroll during streaming, tooltip text requirements, and fixed bottom-right stacked layout.
🚥 Pre-merge checks | ✅ 8
✅ Passed checks (8 passed)
Check name Status Explanation
Title check ✅ Passed Title follows Conventional Commits format (feat scope type) and accurately describes the main changes: adding scroll-to-bottom button and improving scroll navigation logic.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Performance And Algorithmic Complexity ✅ Passed Scroll handler performs O(1) DOM reads with conditional state updates preventing unnecessary re-renders. Message rendering bounded to 100 visible messages via memoization. No N+1 patterns, unbounded growth, or expensive operations detected.
Security And Secret Handling ✅ Passed Pull request modifies only frontend React component with UI-layer functionality (scroll navigation, smooth scrolling, tooltips); no API endpoints, authentication logic, sensitive data, or hardcoded secrets identified.
Kubernetes Resource Safety ✅ Passed PR modifies only frontend React/TypeScript components and specification documents; no infrastructure-as-code changes.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jsell/feat/scroll-navigation
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch jsell/feat/scroll-navigation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/frontend/src/components/session/MessagesTab.tsx`:
- Around line 294-324: The two floating buttons (the one using
showScrollToTop/scrollToTop and the one using isAtBottom/messagesContainerRef)
remain in the tab order when visually hidden; update each Button to be
unfocusable and hidden from assistive tech when its container is hidden by
adding conditional attributes like tabIndex={showScrollToTop ? 0 : -1} (and for
the other button tabIndex={isAtBottom ? -1 : 0}) and
aria-hidden={showScrollToTop ? "false" : "true"} (and aria-hidden for the other
using isAtBottom), or alternatively set disabled when hidden; apply these
changes on the Button elements referenced (the Button inside the TooltipTrigger
for scrollToTop and the Button that scrolls to bottom) so keyboard users cannot
focus hidden controls and tooltips won’t be exposed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 1a58254d-0078-4667-9a59-e5304b6adbbb

📥 Commits

Reviewing files that changed from the base of the PR and between 3d9d991 and 1a8659d.

📒 Files selected for processing (2)
  • components/frontend/src/components/session/MessagesTab.tsx
  • specs/frontend/sessions/messages/scroll-navigation.spec.md

Comment on lines +294 to +324
<div className={`transition-all duration-200 ${showScrollToTop ? "opacity-100 scale-100" : "opacity-0 scale-75 pointer-events-none"}`}>
<Tooltip open={showScrollToTop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-sm"
onClick={scrollToTop}
aria-label="Scroll to top"
className="rounded-full shadow-md cursor-pointer"
>
<ChevronUp className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">Scroll to top</TooltipContent>
</Tooltip>
</div>
<div className={`transition-all duration-200 ${!isAtBottom ? "opacity-100 scale-100" : "opacity-0 scale-75 pointer-events-none"}`}>
<Tooltip open={isAtBottom ? false : undefined}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-sm"
onClick={() => messagesContainerRef.current?.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior: "smooth" })}
aria-label="Scroll to bottom"
className="rounded-full shadow-md cursor-pointer"
>
<ChevronDown className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">Scroll to bottom</TooltipContent>
</Tooltip>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the hidden buttons unfocusable.

pointer-events-none only blocks mouse input. Both buttons still stay in the tab order while invisible, so keyboard users can land on hidden controls and potentially surface the tooltip.

Fix
-                <Button
+                <Button
                   variant="outline"
                   size="icon-sm"
                   onClick={scrollToTop}
                   aria-label="Scroll to top"
+                  tabIndex={showScrollToTop ? 0 : -1}
+                  aria-hidden={!showScrollToTop}
                   className="rounded-full shadow-md cursor-pointer"
                 >
...
-                <Button
+                <Button
                   variant="outline"
                   size="icon-sm"
                   onClick={() => messagesContainerRef.current?.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior: "smooth" })}
                   aria-label="Scroll to bottom"
+                  tabIndex={!isAtBottom ? 0 : -1}
+                  aria-hidden={isAtBottom}
                   className="rounded-full shadow-md cursor-pointer"
                 >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className={`transition-all duration-200 ${showScrollToTop ? "opacity-100 scale-100" : "opacity-0 scale-75 pointer-events-none"}`}>
<Tooltip open={showScrollToTop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-sm"
onClick={scrollToTop}
aria-label="Scroll to top"
className="rounded-full shadow-md cursor-pointer"
>
<ChevronUp className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">Scroll to top</TooltipContent>
</Tooltip>
</div>
<div className={`transition-all duration-200 ${!isAtBottom ? "opacity-100 scale-100" : "opacity-0 scale-75 pointer-events-none"}`}>
<Tooltip open={isAtBottom ? false : undefined}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-sm"
onClick={() => messagesContainerRef.current?.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior: "smooth" })}
aria-label="Scroll to bottom"
className="rounded-full shadow-md cursor-pointer"
>
<ChevronDown className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">Scroll to bottom</TooltipContent>
</Tooltip>
<div className={`transition-all duration-200 ${showScrollToTop ? "opacity-100 scale-100" : "opacity-0 scale-75 pointer-events-none"}`}>
<Tooltip open={showScrollToTop ? undefined : false}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-sm"
onClick={scrollToTop}
aria-label="Scroll to top"
tabIndex={showScrollToTop ? 0 : -1}
aria-hidden={!showScrollToTop}
className="rounded-full shadow-md cursor-pointer"
>
<ChevronUp className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">Scroll to top</TooltipContent>
</Tooltip>
</div>
<div className={`transition-all duration-200 ${!isAtBottom ? "opacity-100 scale-100" : "opacity-0 scale-75 pointer-events-none"}`}>
<Tooltip open={isAtBottom ? false : undefined}>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon-sm"
onClick={() => messagesContainerRef.current?.scrollTo({ top: messagesContainerRef.current.scrollHeight, behavior: "smooth" })}
aria-label="Scroll to bottom"
tabIndex={!isAtBottom ? 0 : -1}
aria-hidden={isAtBottom}
className="rounded-full shadow-md cursor-pointer"
>
<ChevronDown className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">Scroll to bottom</TooltipContent>
</Tooltip>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/components/session/MessagesTab.tsx` around lines 294
- 324, The two floating buttons (the one using showScrollToTop/scrollToTop and
the one using isAtBottom/messagesContainerRef) remain in the tab order when
visually hidden; update each Button to be unfocusable and hidden from assistive
tech when its container is hidden by adding conditional attributes like
tabIndex={showScrollToTop ? 0 : -1} (and for the other button
tabIndex={isAtBottom ? -1 : 0}) and aria-hidden={showScrollToTop ? "false" :
"true"} (and aria-hidden for the other using isAtBottom), or alternatively set
disabled when hidden; apply these changes on the Button elements referenced (the
Button inside the TooltipTrigger for scrollToTop and the Button that scrolls to
bottom) so keyboard users cannot focus hidden controls and tooltips won’t be
exposed.

@mergify mergify Bot added the queued label May 2, 2026
@mergify
Copy link
Copy Markdown
Contributor

mergify Bot commented May 2, 2026

Merge Queue Status

  • Entered queue2026-05-02 02:10 UTC · Rule: default
  • Checks skipped · PR is already up-to-date
  • Merged2026-05-02 02:10 UTC · at 1a8659dc3db2256451261b88aaab1b04c36d3d08 · squash

This pull request spent 11 seconds in the queue, including 1 second running CI.

Required conditions to merge

@mergify mergify Bot merged commit 9db12b3 into main May 2, 2026
70 checks passed
@mergify mergify Bot deleted the jsell/feat/scroll-navigation branch May 2, 2026 02:10
@mergify mergify Bot removed the queued label May 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ambient-code:self-reviewed Self-reviewed by Ambient agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant